//TODO: find out the exact license

//**********************************************************************************
//
//OpenSSLKey
// .NET 2.0  OpenSSL Public & Private Key Parser
//
// Copyright (C) 2008  	JavaScience Consulting
//
//***********************************************************************************
//
//  opensslkey.cs
//
//  Reads and parses:
//    (1) OpenSSL PEM or DER public keys
//    (2) OpenSSL PEM or DER traditional SSLeay private keys (encrypted and unencrypted)
//    (3) PKCS #8 PEM or DER encoded private keys (encrypted and unencrypted)
//  Keys in PEM format must have headers/footers .
//  Encrypted Private Key in SSLEay format not supported in DER
//  Removes header/footer lines.
//  For traditional SSLEAY PEM private keys, checks for encrypted format and
//  uses PBE to extract 3DES key.
//  For SSLEAY format, only supports encryption format: DES-EDE3-CBC
//  For PKCS #8, only supports PKCS#5 v2.0  3des.
//  Parses private and public key components and returns .NET RSA object.
//  Creates dummy unsigned certificate linked to private keypair and
//  optionally exports to pkcs #12
//
// See also: 
//  http://www.openssl.org/docs/crypto/pem.html#PEM_ENCRYPTION_FORMAT 
//**************************************************************************************

//See:
//http://www.jensign.com/opensslkey/pemformats.txt
//http://www.jensign.com/opensslkey/opensslkey.cs
//http://www.bo.infn.it/alice/introgrd/certmgr/node2.html

/* The original file has been extensively modified */

using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.Security;
using System.Diagnostics;
using System.ComponentModel;

//TODO: cleanup
namespace Anculus.Core
{
	internal static class OpenSslRsaKey
	{
		private const string pemp8header	= "-----BEGIN PRIVATE KEY-----";
		private const string pemp8footer	= "-----END PRIVATE KEY-----";
		private const string pempubheader	= "-----BEGIN PUBLIC KEY-----";
		private const string pempubfooter	= "-----END PUBLIC KEY-----";
		private const string pemprivheader	= "-----BEGIN RSA PRIVATE KEY-----";
		private const string pemprivfooter	= "-----END RSA PRIVATE KEY-----";
		
		internal static RSACryptoServiceProvider FromPemFile (string fileName)
		{
			string pemstr = null;
			using (StreamReader sr = File.OpenText (fileName)) {
				pemstr = sr.ReadToEnd ().Trim ();
			}
			
			return DecodePEMKey (pemstr);
		}
		
		internal static void SaveToPemFile (RSACryptoServiceProvider cryptoProvider)
		{
			throw new NotImplementedException ();
		}

		// ------- Decode PEM pubic, private or pkcs8 key ----------------
		private static RSACryptoServiceProvider DecodePEMKey (String pemstr)
		{
			byte[] pempublickey;
			byte[] pemprivatekey;
			byte[] pkcs8privatekey;
			
			if (pemstr.StartsWith (pempubheader) && pemstr.EndsWith (pempubfooter)) {
				pempublickey = DecodeOpenSSLPublicKey (pemstr);
				if (pempublickey != null)
					return DecodeX509PublicKey (pempublickey);
			} else if (pemstr.StartsWith (pemprivheader) && pemstr.EndsWith (pemprivfooter)) {
				pemprivatekey = DecodeOpenSSLPrivateKey (pemstr);
				if (pemprivatekey != null)
					return DecodeRSAPrivateKey (pemprivatekey);
			} else if (pemstr.StartsWith (pemp8header) && pemstr.EndsWith (pemp8footer)) {
				pkcs8privatekey = DecodePkcs8PrivateKey (pemstr);
				if (pkcs8privatekey != null)
					return DecodePrivateKeyInfo (pkcs8privatekey);
			}
			
			return null;
		}


		//--------   Get the binary PKCS #8 PRIVATE key   --------
		private static byte[] DecodePkcs8PrivateKey (String instr)
		{
			String pemstr = instr.Trim ();
			byte[] binkey;
			if (!pemstr.StartsWith (pemp8header) || !pemstr.EndsWith (pemp8footer))
				return null;
			StringBuilder sb = new StringBuilder (pemstr);
			sb.Replace (pemp8header, "");
			//remove headers/footers, if present
			sb.Replace (pemp8footer, "");
			
			String pubstr = sb.ToString ().Trim ();
			//get string after removing leading/trailing whitespace
			try {
				binkey = Convert.FromBase64String (pubstr);
			} catch (System.FormatException) {
				//if can't b64 decode, data is not valid
				return null;
			}
			return binkey;
		}

		//------- Parses binary asn.1 PKCS #8 PrivateKeyInfo; returns RSACryptoServiceProvider ---
		private static RSACryptoServiceProvider DecodePrivateKeyInfo (byte[] pkcs8)
		{
			// encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
			// this byte[] includes the sequence byte and terminal encoded null 
			byte[] SeqOID = {
				0x30,
				0xd,
				0x6,
				0x9,
				0x2a,
				0x86,
				0x48,
				0x86,
				0xf7,
				0xd,
				0x1,
				0x1,
				0x1,
				0x5,
				0x0
			};
			byte[] seq = new byte[15];
			// ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
			MemoryStream mem = new MemoryStream (pkcs8);
			int lenstream = (int)mem.Length;
			BinaryReader binr = new BinaryReader (mem);
			//wrap Memory Stream with BinaryReader for easy reading
			byte bt = 0;
			ushort twobytes = 0;
			
			try {
				
				twobytes = binr.ReadUInt16 ();
				if (twobytes == 0x8130)
					//data read as little endian order (actual data order for Sequence is 30 81)
				//advance 1 byte
					binr.ReadByte (); else if (twobytes == 0x8230)
					binr.ReadInt16 ();
				else
					//advance 2 bytes
					return null;
				
				
				bt = binr.ReadByte ();
				if (bt != 0x2)
					return null;
				
				twobytes = binr.ReadUInt16 ();
				
				if (twobytes != 0x1)
					return null;
				
				seq = binr.ReadBytes (15);
				//read the Sequence OID
				if (!CompareBytearrays (seq, SeqOID))
					//make sure Sequence for OID is correct
					return null;
				
				bt = binr.ReadByte ();
				if (bt != 0x4)
					//expect an Octet string 
					return null;
				
				bt = binr.ReadByte ();
				//read next byte, or next 2 bytes is  0x81 or 0x82; otherwise bt is the byte count
				if (bt == 0x81)
					binr.ReadByte (); else if (bt == 0x82)
					binr.ReadUInt16 ();
				//------ at this stage, the remaining sequence should be the RSA private key
				
				byte[] rsaprivkey = binr.ReadBytes ((int)(lenstream - mem.Position));
				RSACryptoServiceProvider rsacsp = DecodeRSAPrivateKey (rsaprivkey);
				return rsacsp;
			
			} catch (Exception) {
				return null;
			
			} finally {
				binr.Close ();
			}
			
		}

		//--------   Get the binary RSA PUBLIC key   --------
		private static byte[] DecodeOpenSSLPublicKey (String instr)
		{
			String pemstr = instr.Trim ();
			byte[] binkey;
			if (!pemstr.StartsWith (pempubheader) || !pemstr.EndsWith (pempubfooter))
				return null;
			StringBuilder sb = new StringBuilder (pemstr);
			sb.Replace (pempubheader, "");
			//remove headers/footers, if present
			sb.Replace (pempubfooter, "");
			
			String pubstr = sb.ToString ().Trim ();
			//get string after removing leading/trailing whitespace
			try {
				binkey = Convert.FromBase64String (pubstr);
			} catch (System.FormatException) {
				//if can't b64 decode, data is not valid
				return null;
			}
			return binkey;
		}

		//------- Parses binary asn.1 X509 SubjectPublicKeyInfo; returns RSACryptoServiceProvider ---
		public static RSACryptoServiceProvider DecodeX509PublicKey (byte[] x509key)
		{
			// encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
			byte[] SeqOID = {
				0x30,
				0xd,
				0x6,
				0x9,
				0x2a,
				0x86,
				0x48,
				0x86,
				0xf7,
				0xd,
				0x1,
				0x1,
				0x1,
				0x5,
				0x0
			};
			byte[] seq = new byte[15];
			// ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
			MemoryStream mem = new MemoryStream (x509key);
			BinaryReader binr = new BinaryReader (mem);
			//wrap Memory Stream with BinaryReader for easy reading
			byte bt = 0;
			ushort twobytes = 0;
			
			try {
				
				twobytes = binr.ReadUInt16 ();
				if (twobytes == 0x8130)
					//data read as little endian order (actual data order for Sequence is 30 81)
				//advance 1 byte
					binr.ReadByte (); else if (twobytes == 0x8230)
					binr.ReadInt16 ();
				else
					//advance 2 bytes
					return null;
				
				seq = binr.ReadBytes (15);
				//read the Sequence OID
				if (!CompareBytearrays (seq, SeqOID))
					//make sure Sequence for OID is correct
					return null;
				
				twobytes = binr.ReadUInt16 ();
				if (twobytes == 0x8103)
					//data read as little endian order (actual data order for Bit String is 03 81)
				//advance 1 byte
					binr.ReadByte (); else if (twobytes == 0x8203)
					binr.ReadInt16 ();
				else
					//advance 2 bytes
					return null;
				
				bt = binr.ReadByte ();
				if (bt != 0x0)
					//expect null byte next
					return null;
				
				twobytes = binr.ReadUInt16 ();
				if (twobytes == 0x8130)
					//data read as little endian order (actual data order for Sequence is 30 81)
				//advance 1 byte
					binr.ReadByte (); else if (twobytes == 0x8230)
					binr.ReadInt16 ();
				else
					//advance 2 bytes
					return null;
				
				twobytes = binr.ReadUInt16 ();
				byte lowbyte = 0x0;
				byte highbyte = 0x0;
				
				if (twobytes == 0x8102)
					//data read as little endian order (actual data order for Integer is 02 81)
				// read next bytes which is bytes in modulus
					lowbyte = binr.ReadByte (); else if (twobytes == 0x8202) {
					highbyte = binr.ReadByte ();
					//advance 2 bytes
					lowbyte = binr.ReadByte ();
				} else
					return null;
				byte[] modint = {
					lowbyte,
					highbyte,
					0x0,
					0x0
				};
				//reverse byte order since asn.1 key uses big endian order
				int modsize = BitConverter.ToInt32 (modint, 0);
				
				byte firstbyte = binr.ReadByte ();
				binr.BaseStream.Seek (-1, SeekOrigin.Current);
				
				if (firstbyte == 0x0) {
					//if first byte (highest order) of modulus is zero, don't include it
					binr.ReadByte ();
					//skip this null byte
					modsize -= 1;
					//reduce modulus buffer size by 1
				}
				
				byte[] modulus = binr.ReadBytes (modsize);
				//read the modulus bytes
				if (binr.ReadByte () != 0x2)
					//expect an Integer for the exponent data
					return null;
				int expbytes = (int)binr.ReadByte ();
				// should only need one byte for actual exponent data (for all useful values)
				byte[] exponent = binr.ReadBytes (expbytes);
				
				// ------- create RSACryptoServiceProvider instance and initialize with public key -----
				RSACryptoServiceProvider RSA = new RSACryptoServiceProvider ();
				RSAParameters RSAKeyInfo = new RSAParameters ();
				RSAKeyInfo.Modulus = modulus;
				RSAKeyInfo.Exponent = exponent;
				RSA.ImportParameters (RSAKeyInfo);
				return RSA;
			} catch (Exception) {
				return null;
			
			} finally {
				binr.Close ();
			}
			
		}

		//------- Parses binary ans.1 RSA private key; returns RSACryptoServiceProvider  ---
		private static RSACryptoServiceProvider DecodeRSAPrivateKey (byte[] privkey)
		{
			byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;
			
			// ---------  Set up stream to decode the asn.1 encoded RSA private key  ------
			MemoryStream mem = new MemoryStream (privkey);
			BinaryReader binr = new BinaryReader (mem);
			//wrap Memory Stream with BinaryReader for easy reading
			byte bt = 0;
			ushort twobytes = 0;
			int elems = 0;
			try {
				twobytes = binr.ReadUInt16 ();
				if (twobytes == 0x8130)
					//data read as little endian order (actual data order for Sequence is 30 81)
				//advance 1 byte
					binr.ReadByte (); else if (twobytes == 0x8230)
					binr.ReadInt16 ();
				else
					//advance 2 bytes
					return null;
				
				twobytes = binr.ReadUInt16 ();
				if (twobytes != 0x102)
					//version number
					return null;
				bt = binr.ReadByte ();
				if (bt != 0x0)
					return null;

				//------  all private key components are Integer sequences ----
				elems = GetIntegerSize (binr);
				MODULUS = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				E = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				D = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				P = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				Q = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				DP = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				DQ = binr.ReadBytes (elems);
				
				elems = GetIntegerSize (binr);
				IQ = binr.ReadBytes (elems);
				
				// ------- create RSACryptoServiceProvider instance and initialize with public key -----
				RSACryptoServiceProvider RSA = new RSACryptoServiceProvider ();
				RSAParameters RSAparams = new RSAParameters ();
				RSAparams.Modulus = MODULUS;
				RSAparams.Exponent = E;
				RSAparams.D = D;
				RSAparams.P = P;
				RSAparams.Q = Q;
				RSAparams.DP = DP;
				RSAparams.DQ = DQ;
				RSAparams.InverseQ = IQ;
				RSA.ImportParameters (RSAparams);
				return RSA;
			} catch (Exception) {
				return null;
			} finally {
				binr.Close ();
			}
		}

		private static int GetIntegerSize (BinaryReader binr)
		{
			byte bt = 0;
			byte lowbyte = 0x0;
			byte highbyte = 0x0;
			int count = 0;
			bt = binr.ReadByte ();
			if (bt != 0x2)
				//expect integer
				return 0;
			bt = binr.ReadByte ();
			
			if (bt == 0x81)
			// data size in next byte
				count = binr.ReadByte (); else if (bt == 0x82) {
				highbyte = binr.ReadByte ();
				// data size in next 2 bytes
				lowbyte = binr.ReadByte ();
				byte[] modint = {
					lowbyte,
					highbyte,
					0x0,
					0x0
				};
				count = BitConverter.ToInt32 (modint, 0);
			} else {
				count = bt;
				// we already have the data size
			}
			
			
			
			while (binr.ReadByte () == 0x0) {
				//remove high order zeros in data
				count -= 1;
			}
			binr.BaseStream.Seek (-1, SeekOrigin.Current);
			//last ReadByte wasn't a removed zero, so back up a byte
			return count;
		}

		//-----  Get the binary RSA PRIVATE key, decrypting if necessary ----
		private static byte[] DecodeOpenSSLPrivateKey (String instr)
		{
			String pemstr = instr.Trim ();
			byte[] binkey;
			if (!pemstr.StartsWith (pemprivheader) || !pemstr.EndsWith (pemprivfooter))
				return null;
			
			StringBuilder sb = new StringBuilder (pemstr);
			sb.Replace (pemprivheader, "");
			//remove headers/footers, if present
			sb.Replace (pemprivfooter, "");
			
			String pvkstr = sb.ToString ().Trim ();
			//get string after removing leading/trailing whitespace
			try {
				// if there are no PEM encryption info lines, this is an UNencrypted PEM private key
				binkey = Convert.FromBase64String (pvkstr);
				return binkey;
			} catch (System.FormatException) {
				//if can't b64 decode, it must be an encrypted private key
				//Console.WriteLine("Not an unencrypted OpenSSL PEM private key");  
			}
			
			StringReader str = new StringReader (pvkstr);
			
			//-------- read PEM encryption info. lines and extract salt -----
			if (!str.ReadLine ().StartsWith ("Proc-Type: 4,ENCRYPTED"))
				return null;
			String saltline = str.ReadLine ();
			if (!saltline.StartsWith ("DEK-Info: DES-EDE3-CBC,"))
				return null;
			String saltstr = saltline.Substring (saltline.IndexOf (",") + 1).Trim ();
			byte[] salt = new byte[saltstr.Length / 2];
			for (int i = 0; i < salt.Length; i++)
				salt[i] = Convert.ToByte (saltstr.Substring (i * 2, 2), 16);
			if (!(str.ReadLine () == ""))
				return null;
			
			//------ remaining b64 data is encrypted RSA key ----
			String encryptedstr = str.ReadToEnd ();
			
			try {
				//should have b64 encrypted RSA key now
				binkey = Convert.FromBase64String (encryptedstr);
			} catch (System.FormatException) {
				// bad b64 data.
				return null;
			}
			
			//------ Get the 3DES 24 byte key using PDK used by OpenSSL ----
			
			SecureString despswd = new SecureString (); //TODO: password
			//Console.Write("\nEnter password to derive 3DES key: ");
			//String pswd = Console.ReadLine();
			byte[] deskey = GetOpenSSL3deskey (salt, despswd, 1, 2);
			// count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes
			if (deskey == null)
				return null;
			//showBytes("3DES key", deskey) ;
			
			//------ Decrypt the encrypted 3des-encrypted RSA private key ------
			byte[] rsakey = DecryptKey (binkey, deskey, salt);
			//OpenSSL uses salt value in PEM header also as 3DES IV
			if (rsakey != null)
				return rsakey;
			//we have a decrypted RSA private key
			else {
				Console.WriteLine ("Failed to decrypt RSA private key; probably wrong password.");
				return null;
			}
		}

		// ----- Decrypt the 3DES encrypted RSA private key ----------
		private static byte[] DecryptKey (byte[] cipherData, byte[] desKey, byte[] IV)
		{
			MemoryStream memst = new MemoryStream ();
			TripleDES alg = TripleDES.Create ();
			alg.Key = desKey;
			alg.IV = IV;
			try {
				CryptoStream cs = new CryptoStream (memst, alg.CreateDecryptor (), CryptoStreamMode.Write);
				cs.Write (cipherData, 0, cipherData.Length);
				cs.Close ();
			} catch (Exception exc) {
				Console.WriteLine (exc.Message);
				return null;
			}
			byte[] decryptedData = memst.ToArray ();
			return decryptedData;
		}

		//-----   OpenSSL PBKD uses only one hash cycle (count); miter is number of iterations required to build sufficient bytes ---
		private static byte[] GetOpenSSL3deskey (byte[] salt, SecureString secpswd, int count, int miter)
		{
			IntPtr unmanagedPswd = IntPtr.Zero;
			int HASHLENGTH = 16;
			//MD5 bytes
			byte[] keymaterial = new byte[HASHLENGTH * miter];
			//to store contatenated Mi hashed results
			
			byte[] psbytes = new byte[secpswd.Length];
			unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi (secpswd);
			Marshal.Copy (unmanagedPswd, psbytes, 0, psbytes.Length);
			Marshal.ZeroFreeGlobalAllocAnsi (unmanagedPswd);
			
			//UTF8Encoding utf8 = new UTF8Encoding();
			//byte[] psbytes = utf8.GetBytes(pswd);
			
			// --- contatenate salt and pswd bytes into fixed data array ---
			byte[] data00 = new byte[psbytes.Length + salt.Length];
			Array.Copy (psbytes, data00, psbytes.Length);
			//copy the pswd bytes
			Array.Copy (salt, 0, data00, psbytes.Length, salt.Length);
			//concatenate the salt bytes
			// ---- do multi-hashing and contatenate results  D1, D2 ...  into keymaterial bytes ----
			MD5 md5 = new MD5CryptoServiceProvider ();
			byte[] result = null;
			byte[] hashtarget = new byte[HASHLENGTH + data00.Length];
			//fixed length initial hashtarget
			for (int j = 0; j < miter; j++) {
				// ----  Now hash consecutively for count times ------
				if (j == 0)
					result = data00;
				//initialize 
				else {
					Array.Copy (result, hashtarget, result.Length);
					Array.Copy (data00, 0, hashtarget, result.Length, data00.Length);
					result = hashtarget;
					//Console.WriteLine("Updated new initial hash target:") ;
					//showBytes(result) ;
				}
				
				for (int i = 0; i < count; i++)
					result = md5.ComputeHash (result);
				Array.Copy (result, 0, keymaterial, j * HASHLENGTH, result.Length);
				//contatenate to keymaterial
			}
			//showBytes("Final key material", keymaterial);
			byte[] deskey = new byte[24];
			Array.Copy (keymaterial, deskey, deskey.Length);
			
			Array.Clear (psbytes, 0, psbytes.Length);
			Array.Clear (data00, 0, data00.Length);
			Array.Clear (result, 0, result.Length);
			Array.Clear (hashtarget, 0, hashtarget.Length);
			Array.Clear (keymaterial, 0, keymaterial.Length);
			
			return deskey;
		}

		private static bool CompareBytearrays (byte[] a, byte[] b)
		{
			if (a.Length != b.Length)
				return false;
			int i = 0;
			foreach (byte c in a) {
				if (c != b[i])
					return false;
				i++;
			}
			return true;
		}
	}
}

